package com.studerb.service;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.Logger;
import org.bushe.swing.event.annotation.EventSubscriber;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import com.studerb.dao.interfaces.INewsGroupDao;
import com.studerb.events.NewsGroupEvent;
import com.studerb.events.NntpConnectionEvent;
import com.studerb.exceptions.ServiceException;
import com.studerb.model.NewsGroup;
import com.studerb.nntp.GroupCommandReply;
import com.studerb.service.interfaces.IHeaderService;
import com.studerb.service.interfaces.INewsGroupService;
import com.studerb.service.interfaces.ISettingsService;
import com.studerb.service.interfaces.ITaskService;
import com.studerb.tasks.TaskType;
import com.studerb.tasks.interfaces.ITask;
import com.studerb.tasks.interfaces.ITaskFactory;
import com.studerb.tasks.interfaces.ITaskListener;
import com.studerb.util.Constants;

public class NewsGroupService implements INewsGroupService {
	public static int BATCH_SIZE;

	private final static Logger logger = Logger.getLogger(NewsGroupService.class);
	public final static int MAX_NAME_LENGTH = 120;
	public static int MIN_SIZE;
	public static Boolean ONLY_BINARIES;

	@Autowired private EventService eventService;
	@Autowired private IHeaderService headerService;
	@Autowired private INewsGroupDao newsGroupDao;
	@Autowired private ISettingsService settingsService;
	@Autowired private ITaskFactory taskFactory;
	@Autowired protected PlatformTransactionManager transactionManager;
	@Autowired private ITaskService taskService;

	private TransactionTemplate transactionTemplate;
	private List<NewsGroup> newsGroups;
	private boolean started = false;

	// Can not use @Transactional here because this method called
	// during Spring 'init' method before Transactional is set up...
	synchronized private void _refreshGroups() {
		newsGroups = newsGroupDao.getAllByOrder("groupName", "asc");
		logger.debug("Refreshed groups: " + newsGroups.size() + " newsgroups in the database");
		eventService.publish(new NewsGroupEvent(NewsGroupEvent.EventType.ALL_REFRESHED));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.studerb.service.interfaces.INewsGroupService#downloadNewsGroups(com
	 * .studerb.tasks.interfaces.ITaskListener)
	 */
	@Override public void downloadAllNewsGroups(ITaskListener... taskListeners) throws ServiceException {
		logger.debug("Creating LIST_COMMAND task");
		ITask task = taskFactory.getTask(TaskType.LIST_COMMAND);
		for (ITaskListener taskListener : taskListeners) {
			task.addTaskListener(taskListener);
		}
		boolean success = taskService.addTask(task);
		if (!success) {
			throw new ServiceException("Error adding Task to task service");
		}
	}

	@Transactional(readOnly = true) @Override public NewsGroup getByGroupName(String groupName) throws ServiceException {
		NewsGroup ng = newsGroupDao.getByGroupName(groupName);
		if (ng == null) {
			throw new ServiceException("No newsgroup with name: " + groupName);
		}
		return ng;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.studerb.service.interfaces.INewsGroupService#getHeaderCount(com.studerb
	 * .model.NewsGroup)
	 */
	@Transactional(readOnly = true) @Override public int getHeaderCount(NewsGroup group) throws ServiceException {
		int index = newsGroups.indexOf(group);
		if (index < 0) {
			throw new ServiceException("NewsGroup: " + group.getGroupName() + " does not exist");
		}
		return newsGroupDao.getHeaderCount(group);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.studerb.service.interfaces.IService#stop()
	 */
	@Override public Callable<Void> getListCommandProcessor(BlockingQueue<String> queue) {
		return new ListCommandProcessor(queue);
	}

	@Transactional(readOnly = true) @Override public Long getMaxArticleNumber(NewsGroup newsGroup) {
		return newsGroupDao.getMaxArticleNumber(newsGroup);
	}

	@Transactional(readOnly = true) @Override public Long getMinArticleNumber(NewsGroup newsGroup) {
		return newsGroupDao.getMinArticleNumber(newsGroup);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.studerb.service.interfaces.INewsGroupService#getNewsGroups()
	 */
	@Override @Transactional(readOnly = true) public List<NewsGroup> getNewsGroups() {
		if (newsGroups == null) {
			_refreshGroups();
		}
		return Collections.unmodifiableList(newsGroups);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.studerb.service.interfaces.IService#isStarted()
	 */
	@Override public boolean isStarted() {
		return started;
	}

	@EventSubscriber(eventServiceName = Constants.EVENT_SERVICE_NAME) public void onEvent(NntpConnectionEvent evt) {
		NntpConnectionEvent.EventType eventType = evt.getType();

		switch (eventType) {
		case GROUP:
			updateGroupCommandReply(evt);
			break;
		default:
			break;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.studerb.service.interfaces.IService#start()
	 */
	@Override public void start() {
		if (started) {
			throw new RuntimeException("NewsGroupService already Started");
		}
		logger.info("[**NewsGroupService Starting**]");
		transactionTemplate = new TransactionTemplate(transactionManager);
		try {
			ONLY_BINARIES = Boolean.parseBoolean(settingsService.getSetting("groups.only.binaries"));
			MIN_SIZE = Integer.parseInt(settingsService.getSetting("groups.min.size"));
			BATCH_SIZE = Integer.parseInt(settingsService.getSetting(Constants.DB_BATCH_SIZE));
			started = true;
		}
		catch (Exception e) {
			started = false;
			throw new RuntimeException("Error initializing NewsGroups from Database", e);
		}
	}

	@Override public void stop() {
		if (!started) {
			throw new RuntimeException("NewsGroupService already Started");
		}
		logger.info("[**NewsGroupService Stopping**]");
		started = false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.studerb.service.interfaces.INewsGroupService#subscribe(com.studerb
	 * .model.NewsGroup)
	 */
	@Transactional public void subscribe(NewsGroup newsGroup) throws ServiceException {
		logger.debug("subscribing newsGroup: " + newsGroup.getGroupName());
		int index = newsGroups.indexOf(newsGroup);
		if (index < 0) {
			throw new ServiceException("NewsGroup: " + newsGroup.getGroupName() + " does not exist");
		}
		NewsGroup temp = newsGroups.get(index);
		newsGroup.setSubscribed(true);
		try {
			newsGroupDao.update(temp);
			NewsGroupEvent event = new NewsGroupEvent(NewsGroupEvent.EventType.UPDATED, newsGroup);
			eventService.publish(event);
		}
		catch (Exception e) {
			throw new ServiceException("Error subscribing " + temp.getGroupName() + ":  " + ExceptionUtils.getRootCauseMessage(e));
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.studerb.service.interfaces.INewsGroupService#unsubscribe(com.studerb
	 * .model.NewsGroup)
	 */
	@Transactional public void unsubscribe(NewsGroup newsGroup) {
		logger.debug("unsubscribing newsGroup: " + newsGroup.getGroupName());
		int index = newsGroups.indexOf(newsGroup);
		if (index < 0) {
			throw new ServiceException("NewsGroup: " + newsGroup.getGroupName() + " does not exist");
		}
		Long id = newsGroups.get(index).getId();
		NewsGroup temp = null;
		newsGroup.setSubscribed(false);
		try {
			temp = newsGroupDao.read(id);
			headerService.deleteAllPosts(temp);
			temp.setSubscribed(false);
			eventService.publish(new NewsGroupEvent(NewsGroupEvent.EventType.UPDATED, newsGroup));
		}
		catch (Exception e) {
			throw new ServiceException("Error subscribing " + temp.getGroupName() + ":  " + ExceptionUtils.getRootCauseMessage(e));
		}
	}

	private void updateGroupCommandReply(NntpConnectionEvent evt) {
		GroupCommandReply groupCommandReply = evt.getGroupCommandReply();
		NewsGroup temp = new NewsGroup();
		temp.setGroupName(groupCommandReply.getGroupName());
		int index = newsGroups.indexOf(new NewsGroup());
		if (index < 0) {
			throw new RuntimeException("Error finding group: " + groupCommandReply.getGroupName());
		}
		NewsGroup newsGroup = newsGroups.get(index);
		newsGroup.setPostsCount(groupCommandReply.getArticleCount());
		updateNewsGroup(newsGroup);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.studerb.service.interfaces.INewsGroupService#updateNewsGroup(com.
	 * studerb.model.NewsGroup)
	 */
	@Transactional synchronized public void updateNewsGroup(NewsGroup newsGroup) throws ServiceException {
		if (newsGroups.indexOf(newsGroup) < 0) {
			throw new ServiceException("Cannot update non-existing group: " + newsGroup);
		}
		newsGroupDao.update(newsGroup);
		eventService.publish(new NewsGroupEvent(NewsGroupEvent.EventType.UPDATED, newsGroup));
	}

	class ListCommandProcessor implements Callable<Void> {
		private final BlockingQueue<String> queue;

		ListCommandProcessor(BlockingQueue<String> queue) {
			this.queue = queue;
		}

		@Override public Void call() throws Exception {
			final List<NewsGroup> tempNewsGroups = new ArrayList<NewsGroup>();
			final Calendar started = Calendar.getInstance();
			int created = 0;
			int updated = 0;
			String nextLine = null;
			try {
				do {
					nextLine = queue.take();
					NewsGroup tempNg = NewsGroup.createNewsGroup(nextLine);
					if (tempNg != null) {
						int index = newsGroups.indexOf(tempNg);
						if (index != -1) {
							NewsGroup oldNg = newsGroups.get(index);
							oldNg.setCanPost(tempNg.getCanPost());
							oldNg.setLastUpdated(started);
							tempNewsGroups.add(oldNg);
							updated++;
						}
						else {
							tempNewsGroups.add(tempNg);
							created++;
						}
					}
					if (!tempNewsGroups.isEmpty()) {
						if (tempNewsGroups.size() % BATCH_SIZE == 0 || nextLine.equals(Constants.END_OF_QUEUE)) {
							transactionTemplate.execute(new TransactionCallbackWithoutResult() {
								@Override protected void doInTransactionWithoutResult(TransactionStatus arg0) {
									logger.debug("Saving/updating: " + tempNewsGroups.size() + " groups");
									for (NewsGroup newsGroup : tempNewsGroups) {
										newsGroupDao.saveOrUpdate(newsGroup);
									}
									tempNewsGroups.clear();
								}
							});

						}
					}

				}
				while (!nextLine.equals(Constants.END_OF_QUEUE));
				logger.info(String.format("Created: %d and updated %d NewsGroups", created, updated));
				// get rid of no longer used groups
				transactionTemplate.execute(new TransactionCallbackWithoutResult() {
					@Override protected void doInTransactionWithoutResult(TransactionStatus arg0) {
						newsGroupDao.deleteAllUnsubscribedByLastUpdated(started);
						_refreshGroups();
					}
				});

			}
			finally {
				// remove the queue so that the parent task is not blocked
				queue.clear();
			}
			return null;
		}
	}
}
